package jmine.tec.persist.result;

import java.lang.reflect.Method;

import jmine.tec.aop.AOPMessages;
import jmine.tec.proxy.chain.ChainElement;
import jmine.tec.proxy.chain.InvocationChain;
import jmine.tec.result.ResultException;
import jmine.tec.result.api.CacheStrategy;
import jmine.tec.result.api.Result;
import jmine.tec.result.api.ResultKey;
import bancosys.tec.persist.Persistable;
import bancosys.tec.persist.dao.BaseDAO;
import bancosys.tec.utils.reflection.ReflectionUtils;

/**
 * Interceptor para métodos anotados com {@link Result}. Depende de um {@link CacheStrategy} que irá determinar como os resultados são
 * salvos para uso posterior.
 * 
 * @author jrenaut
 */
public class ResultChainElement implements ChainElement {

    private CacheStrategy cacheStrategy;

    private ResultController resultController;

    /**
     * C'tor
     */
    public ResultChainElement() {
        super();
    }

    /**
     * Executado quando um método anotado com {@link Result} é interceptado.
     * <p>
     * Lê a anotação {@link Result} e constrói o objeto com os campos "chave" preenchidos. Busca no Cache se algum resultado já existe para
     * o mesmo.
     * <p>
     * Se encontrar retorno, senão permite a execução do método sendo invocado e armazena o resultado no Cache para (possível) uso
     * posterior.
     * 
     * @param chain {@link InvocationChain}
     * @return o retorno do método.
     * @throws Throwable não interfere com exceptions decorrentes da chamada ao método sendo invocado
     */
    public Object aroundInvoke(InvocationChain chain) throws Throwable {
        Method method = chain.getInvokingMethod();
        Result resultAnnot = method.getAnnotation(Result.class);
        if (resultAnnot != null) {
            this.validate(chain);
            Object key = this.createResultObject(chain);
            if (key != null) {
                Object value = this.cacheStrategy.getValue(key);
                if (value != null) {
                    return this.getResult(value, resultAnnot);
                }

                Object invocationResult = chain.proceed();
                this.setResult(key, invocationResult, resultAnnot);
                this.cacheStrategy.putValue(key);
                return invocationResult;
            }
        }
        return chain.proceed();
    }

    /**
     * Valida a anotação.
     * 
     * @param chain O {@link InvocationChain}
     */
    private void validate(InvocationChain chain) {
        Method method = chain.getInvokingMethod();
        Result resultAnnot = method.getAnnotation(Result.class);

        if (Void.TYPE.equals(method.getReturnType())) {
            throw new ResultException(AOPMessages.RESULT_INVALID_ANNOTATED_METHOD.create());
        }

        if (chain.getParams() == null) {
            throw new ResultException(AOPMessages.RESULT_INVALID_NO_ARG_METHOD.create());
        }

        String result = resultAnnot.result();
        Class<?> clazz = resultAnnot.clazz();
        if (clazz == null) {
            throw new ResultException(AOPMessages.RESULT_INVALID_RESULT_CLASS.create());
        }
        if (result == null || "".equals(result.trim())) {
            throw new ResultException(AOPMessages.RESULT_INVALID_RESULT_FIELD.create());
        }
        if (resultAnnot.keys().length == 0) {
            throw new ResultException(AOPMessages.RESULT_EMPTY_KEYS.create());
        }
        for (ResultKey key : resultAnnot.keys()) {
            if (key.index() < 0 || key.index() >= chain.getParams().length) {
                throw new ResultException(AOPMessages.RESULT_INDEX_OUT_OF_BOUNDS.create(key.index(), method.getName()));
            }
            String field = key.field();
            if (field == null || "".equals(field.trim())) {
                throw new ResultException(AOPMessages.RESULT_INVALID_KEY_FIELD.create());
            }
        }
    }

    /**
     * Recupera o resultado contido.
     * 
     * @param value Object
     * @param resultAnnot O {@link Result}
     * @return o resultado guardado
     */
    private Object getResult(Object value, Result resultAnnot) {
        return ReflectionUtils.getValue(value, resultAnnot.result());
    }

    /**
     * Armazena no objeto chave o valor da invocação do método.
     * 
     * @param key o objeto chave
     * @param invocationResult o resultado da invocação do método
     * @param resultAnnot {@link Result}
     */
    private void setResult(Object key, Object invocationResult, Result resultAnnot) {
        ReflectionUtils.setValue(key, resultAnnot.result(), invocationResult);
    }

    /**
     * Cria o objeto que representa o resultado desta chamada a método baseado nas informações presentes na anotação {@link Result}.
     * <p>
     * Apenas os campos que fazem parte da "chave" do objeto serão preenchidos.
     * 
     * @param chain {@link InvocationChain}
     * @return Object
     */
    private Object createResultObject(InvocationChain chain) {
        Method method = chain.getInvokingMethod();
        Object[] arguments = chain.getParams();
        Result resultAnnot = method.getAnnotation(Result.class);

        // instancia
        Class<?> resultClass = resultAnnot.clazz();
        Object resultInstance = null;
        if (Persistable.class.isAssignableFrom(resultClass)) {
            @SuppressWarnings("unchecked")
            Class<? extends Persistable> pclass = (Class<? extends Persistable>) resultClass;
            BaseDAO<Persistable> dao = this.resultController.getDAOFactory().getGenericDAO(pclass);
            resultInstance = dao.createBean();
        } else {
            resultInstance = ReflectionUtils.newInstance(resultClass);
        }

        // preenche os campos chave
        for (ResultKey resultKey : resultAnnot.keys()) {
            int index = resultKey.index();
            String field = resultKey.field();
            Object arg = arguments[index];
            if (arg == null || (arg instanceof Persistable && ((Persistable) arg).getPk() == null)) {
                return null;
            }
            ReflectionUtils.setValue(resultInstance, field, arg);
        }

        return resultInstance;
    }

    /**
     * @return the cacheStrategy
     */
    public CacheStrategy getCacheStrategy() {
        return this.cacheStrategy;
    }

    /**
     * @param cacheStrategy the cacheStrategy to set
     */
    public void setCacheStrategy(CacheStrategy cacheStrategy) {
        this.cacheStrategy = cacheStrategy;
    }

    /**
     * @return the resultController
     */
    public ResultController getResultController() {
        return this.resultController;
    }

    /**
     * @param resultController the resultController to set
     */
    public void setResultController(ResultController resultController) {
        this.resultController = resultController;
    }

}
